/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.keycloak.social.dingtalk;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.messages.Messages;

import com.fasterxml.jackson.databind.JsonNode;



/**
 * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
 */
@SuppressWarnings("rawtypes")
public class DingtalkIdentityProvider extends AbstractOAuth2IdentityProvider_dzp implements SocialIdentityProvider {

	public static final String AUTH_URL = "https://oapi.dingtalk.com/connect/qrconnect";
	public static final String PROFILE_URL = "https://oapi.dingtalk.com/sns/getuserinfo_bycode";
	public static final String DEFAULT_SCOPE = "snsapi_login";
	public static final String DINGTALK_PARAMETER_APP_ID = "appid";

	 public static final String PROFILE_NICK = "nick";
	 public static final String PROFILE_OPENID = "openid";
	 public static final String PROFILE_UNIONID = "unionid";

	 //idp 类型-key
     public static final String PROFILE_IDP_TYPE = "idp_type";
     //idp 类型-value
     public static final String IDP_TYPE = "dingtalk";	 
	 //用于标记特定属性前缀
	 public static final String ATTRIBUTE_PREFIX = "dingtalk_";
	@SuppressWarnings("unchecked")
	public DingtalkIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
		super(session, config);
		config.setAuthorizationUrl(AUTH_URL);
		config.setUserInfoUrl(PROFILE_URL);
	}

    @Override
    public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
        return new Endpoint(callback, realm, event);
    }

    @Override
    protected boolean supportsExternalExchange() {
        return true;
    }
    
    
	@Override
	protected String getProfileEndpointForValidation(EventBuilder event) {
		return PROFILE_URL;
	}

	
	
	
	@Override
	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
		String unionid = getJsonProperty(profile, PROFILE_UNIONID);
		String openid = getJsonProperty(profile, PROFILE_OPENID);
		String nick = getJsonProperty(profile, PROFILE_NICK);
		BrokeredIdentityContext user = new BrokeredIdentityContext(unionid);
		user.setUsername(unionid);
		user.setBrokerUserId(unionid);
		user.setModelUsername(unionid);
		user.setEmail(unionid+"@mail.default");
		user.setFirstName(nick!=null?nick:unionid);
		user.setLastName(IDP_TYPE);
		
		//其他属性
        //idp-type
		user.setUserAttribute(PROFILE_IDP_TYPE, IDP_TYPE);
		//用户昵称
		user.setUserAttribute(PROFILE_NICK, nick);
		//用户在当前开放应用所属企业的唯一标识
		user.setUserAttribute(PROFILE_UNIONID, unionid);
		//用户在当前开放应用内的唯一标识
		user.setUserAttribute(PROFILE_OPENID, openid);
		
		//拓展标记
		user.setUserAttribute(ATTRIBUTE_PREFIX+PROFILE_NICK, nick);
		user.setUserAttribute(ATTRIBUTE_PREFIX+PROFILE_UNIONID, unionid);
		
		user.setIdpConfig(getConfig());
		user.setIdp(this);
		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
		return user;
	}


    public BrokeredIdentityContext getFederatedIdentity(String authorizationCode) {
		try {
			// 根据timestamp, appSecret计算签名值
			String timestamp = new Date().getTime()+"";
			String stringToSign = timestamp;
			Mac mac = Mac.getInstance("HmacSHA256");
			mac.init(new SecretKeySpec(getConfig().getClientSecret().getBytes("UTF-8"), "HmacSHA256"));
			byte[] signatureBytes = mac.doFinal(stringToSign.getBytes("UTF-8"));
			String signature = new String(Base64.getEncoder().encode(signatureBytes));
			String urlEncodeSignature = urlEncode(signature,"utf-8");
			
			//获取用户信息
			StringBuffer user_info_url = new StringBuffer(PROFILE_URL+"?");
			user_info_url.append("accessKey="+getConfig().getClientId());
			user_info_url.append("&timestamp="+timestamp);
			user_info_url.append("&signature="+urlEncodeSignature);
			logger.info("-----dingtalk user_info_url : "+user_info_url.toString());
			Map<String, String> map = new HashMap<String, String>();
			map.put("tmp_auth_code", authorizationCode);
			JsonNode profile = SimpleHttp.doPost(user_info_url.toString(), session).json(map).asJson();
			logger.info("---dzp--doGetFederatedIdentity response => "+ profile.toPrettyString());
			
			BrokeredIdentityContext user = extractIdentityFromProfile(null, profile.get("user_info"));
			return user;
		} catch (Exception e) {
			throw new IdentityBrokerException("Could not obtain user profile from dingtalk.", e);
		}
	}


    

    @Override
    public Response performLogin(AuthenticationRequest request) {
        try {
            URI authorizationUrl = createAuthorizationUrl(request).build();
            logger.info("auth url " + authorizationUrl.toString());
            return Response.seeOther(authorizationUrl).build();
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new IdentityBrokerException("Could not create authentication request.", e);
        }
    }
    
    

	@Override
	protected String getDefaultScopes() {
		return DEFAULT_SCOPE;
	}
    
	@Override
    protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
        final UriBuilder uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl())
                .queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
                .queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded())
                .queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")
                .queryParam(DINGTALK_PARAMETER_APP_ID, getConfig().getClientId())
                .queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
        return uriBuilder;
    }
    
    
	
	
    protected class Endpoint {
        protected AuthenticationCallback callback;
        protected RealmModel realm;
        protected EventBuilder event;

        @Context
        protected KeycloakSession session;

        @Context
        protected ClientConnection clientConnection;

        @Context
        protected HttpHeaders headers;

        public Endpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
            this.callback = callback;
            this.realm = realm;
            this.event = event;
        }

        @GET
        public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider_dzp.OAUTH2_PARAMETER_STATE) String state,
                                     @QueryParam(AbstractOAuth2IdentityProvider_dzp.OAUTH2_PARAMETER_CODE) String authorizationCode,
                                     @QueryParam(OAuth2Constants.ERROR) String error) {
            if (error != null) {
                logger.error(error + " for broker login " + getConfig().getProviderId());
                if (error.equals(ACCESS_DENIED)) {
                    return callback.cancelled(state);
                } else if (error.equals(OAuthErrorException.LOGIN_REQUIRED) || error.equals(OAuthErrorException.INTERACTION_REQUIRED)) {
                    return callback.error(state, error);
                } else {
                    return callback.error(state, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
                }
            }

            try {

                BrokeredIdentityContext federatedIdentity;
                if (authorizationCode != null) {
                    federatedIdentity = getFederatedIdentity(authorizationCode);

                    federatedIdentity.setIdpConfig(getConfig());
                    federatedIdentity.setIdp(DingtalkIdentityProvider.this);
                    federatedIdentity.setCode(state);

                    return callback.authenticated(federatedIdentity);
                }
                
            } catch (WebApplicationException e) {
                return e.getResponse();
            } catch (Exception e) {
                logger.error("Failed to make identity provider oauth callback", e);
            }
            event.event(EventType.LOGIN);
            event.error(Errors.IDENTITY_PROVIDER_LOGIN_FAILURE);
            return ErrorPage.error(session, null, Response.Status.BAD_GATEWAY, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
        }

    }
	
	
    @Override
    public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
        return Response.ok("toekn").build();
    }
   
	
	
	

	
	
    @Override
    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
    	user.setSingleAttribute(PROFILE_IDP_TYPE, context.getUserAttribute(PROFILE_IDP_TYPE));
    	user.setSingleAttribute(PROFILE_NICK, context.getUserAttribute(PROFILE_NICK));
        user.setSingleAttribute(PROFILE_UNIONID, context.getUserAttribute(PROFILE_UNIONID));
        user.setSingleAttribute(PROFILE_OPENID, context.getUserAttribute(PROFILE_OPENID));
        
        //拓展属性-用于标记IDP
        user.setSingleAttribute(ATTRIBUTE_PREFIX+PROFILE_NICK, context.getUserAttribute(PROFILE_NICK));
        user.setSingleAttribute(ATTRIBUTE_PREFIX+PROFILE_UNIONID, context.getUserAttribute(PROFILE_UNIONID));
        
        user.setUsername(context.getUsername());
        user.setEmail(context.getEmail());
        user.setFirstName(context.getFirstName());
        user.setLastName(context.getLastName());
    }
	
    
    
	// encoding参数使用utf-8
	private static String urlEncode(String value, String encoding) {
	    if (value == null) {
	        return "";
	    }
	    try {
	        String encoded = URLEncoder.encode(value, encoding);
	        return encoded.replace("+", "%20").replace("*", "%2A")
	            .replace("~", "%7E").replace("/", "%2F");
	    } catch (UnsupportedEncodingException e) {
	        throw new IllegalArgumentException("FailedToEncodeUri", e);
	    }
	}
    
}
